package board;

import java.awt.*;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.*;

import board.command.Command;

import ui.GobanPanel;
import util.Util;

/**
 * This class represents the board, stones, and markup.
 * @author TKington
 *
 */
public class Board
{
	/** The x,y offsets of a stone's liberties. */
    static int [][] offsets = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};
    
    /** The stones on the board.  1 for black, -1 for white. */
    private int [][] board;
    /** The size of the board. */
    private int size;
    /** The marks on the board. */
    private ArrayList<NodeMark> marks = new ArrayList<NodeMark>();
    /** UndoController for undo/redo of Commands. */
    private UndoController undo;
    /** The GobanPanel displaying the Board. */
    private GobanPanel panel;
    
    /** The point, if any, where it is illegal to play on the next move. */
    private Point ko = new Point(-1, -1);
    
    /** The list of points that are correct next moves. */
    private ArrayList<Point> goodMoves = new ArrayList<Point>();
    /** The list of points that are incorrect next moves. */
    private ArrayList<Point> badMoves = new ArrayList<Point>();
    
    /**
     * Creates a Board object of the given size.
     * @param size the size
     */
    public Board(int size)
    {
        this.size = size;
        board = new int[size][size];
        undo = new UndoController();
    }
    
    /**
     * Creates a copy of a Board object.
     * @param b the Board to copy
     */
    public Board(Board b) {
        size = b.size;
        board = new int[size][size];
        for(int i = 0; i < size; i++) {
            for(int j = 0; j < size; j++) {
                board[i][j] = b.board[i][j];
            }
        }
        
        for(NodeMark m : b.marks) {
        	marks.add((NodeMark)m.clone());
        }
    }
    
    /**
     * Sets the GobanPanel associated with this Board.
     * @param p the GobanPanel
     */
    public void setPanel(GobanPanel p) { panel = p; }
    
    /**
     * Executes a Command on the Board.
     * @param c the command to execute
     */
    public void executeCommand(Command c) {
        undo.doCommand(c, this);
        panel.repaint();
    }
    
    /**
     * Undoes the last Command executed on this Board.
     *
     */
    public void undoLast() {
        undo.undo(this);
        panel.repaint();
    }
    
    /**
     * Checks to see if a given move is legal.
     * @param color 1 for black, -1 for white
     * @param x the x location
     * @param y the y location
     * @return true if the move is legal
     */
    public boolean isLegalMove(int color, int x, int y) {
        if(x < 0 || x >= size || y < 0 || y >= size || board[x][y] != 0)
            return false;
        
        if(x == ko.x && y == ko.y)
            return false;
        
        try {
            board[x][y] = color;
            
            if(countLiberties(x, y) > 0)
                return true;
            
            for(int i = 0; i < offsets.length; i++) {
                int nx = x + offsets[i][0];
                int ny = y + offsets[i][1];
                if(nx < 0 || nx >= size || ny < 0 || ny >= size)
                    continue;
                
                if(board[nx][ny] == color * -1 && countLiberties(nx, ny) == 0)
                    return true;
            }
            
            return false;
        }
        finally {
            board[x][y] = 0;
        }
    }
    
    /**
     * Plays a move on the Board.
     * @param color 1 for black, -1 for white
     * @param x the x location
     * @param y the y location
     * @param removedStones list that will be filled with 
     *   the locations of any stones that were captured by making the move
     */
    public void move(int color, int x, int y, ArrayList<Point> removedStones) {
        if(x < 0 || x >= size || y < 0 || y >= size)
            return;
        
        board[x][y] = color;
        
        for(int i = 0; i < offsets.length; i++) {
            int nx = x + offsets[i][0];
            int ny = y + offsets[i][1];
            if(nx < 0 || nx >= size || ny < 0 || ny >= size)
                continue;
            
            if(board[nx][ny] == color * -1 && countLiberties(nx, ny) == 0) {
                removeGroup(nx, ny, removedStones);
            }
        }
        
        if(removedStones.size() == 1 && isAlone(x, y) && countLiberties(x, y) == 1) {
            setKoPoint(x, y);
        }
        else ko.x = -1;
    }
    
    /**
     * Removes the group of stones connected to the stone at x,y, and adds
     * their locations to removedStones.
     * @param x the x location
     * @param y the y location
     * @param removedStones list that will be filled with the
     *   locations of the removed stones
     */
    private void removeGroup(int x, int y, ArrayList<Point> removedStones) {
        int color = board[x][y];
        
        LinkedList<Point> q = new LinkedList<Point>();
        q.add(new Point(x, y));
        
        while(!q.isEmpty()) {
            Point p = q.removeFirst();
            
            for(int i = 0; i < offsets.length; i++) {
                int nx = p.x + offsets[i][0];
                int ny = p.y + offsets[i][1];
                if(nx < 0 || nx >= size || ny < 0 || ny >= size)
                    continue;
                
                if(board[nx][ny] == color)
                    q.addLast(new Point(nx, ny));
            }
            
            removedStones.add(new Point(p.x, p.y));
            board[p.x][p.y] = 0;
        }
    }
    
    /**
     * Checks whether a stone is connected to any stones of the same color.
     * @param x the x location
     * @param y the y location
     * @return true if the stone is alone
     */
    private boolean isAlone(int x, int y) {
        int color = board[x][y];
        
        for(int i = 0; i < offsets.length; i++) {
            int nx = x + offsets[i][0];
            int ny = y + offsets[i][1];
            if(nx < 0 || nx >= size || ny < 0 || ny >= size)
                continue;
            
            if(board[nx][ny] == color)
                return false;
        }
        
        return true;
    }
    
    /**
     * Sets the ko point on the Board.  That is, sets the location where a stone
     * cannot be played on the next move.  The x and y coordinates that are passed
     * in are those of the last stone played, and the ko point will be the only
     * empty neighboring intersection. 
     * @param x the x location of the last stone played
     * @param y the y location of the last stone played
     */
    private void setKoPoint(int x, int y) {
        for(int i = 0; i < offsets.length; i++) {
            int nx = x + offsets[i][0];
            int ny = y + offsets[i][1];
            if(nx < 0 || nx >= size || ny < 0 || ny >= size)
                continue;
            
            if(board[nx][ny] == 0) {
                ko.x = nx;
                ko.y = ny;
                return;
            }       
        }
        
        Util.log(new Exception());
    }
    
    /**
     * Counts the liberties of the string of stones at x,y.
     * @param x the x location
     * @param y the y location
     * @return the number of liberties on the string
     */
    private int countLiberties(int x, int y) {
        int color = board[x][y];
        boolean [][] used = new boolean[size][size];
        
        LinkedList<Point> q = new LinkedList<Point>();
        q.add(new Point(x, y));
        
        int num = 0;
        while(!q.isEmpty()) {
            Point p = q.removeFirst();
            used[p.x][p.y] = true;
            
            for(int i = 0; i < offsets.length; i++) {
                int nx = p.x + offsets[i][0];
                int ny = p.y + offsets[i][1];
                if(nx < 0 || nx >= size || ny < 0 || ny >= size)
                    continue;
                
                if(board[nx][ny] == color && !used[nx][ny])
                    q.addLast(new Point(nx, ny));
                else if(board[nx][ny] == 0 && !used[nx][ny]) {
                    num++;
                    used[nx][ny] = true;
                }
            }
        }
        
        return num;
    }
    
    /**
     * Removes the stone at x,y
     * @param x the x location
     * @param y the y location
     * @return the color of the stone
     */
    public int remove(int x, int y) {
        int old = board[x][y];
        board[x][y] = 0;
        return old;
    }
    
    /**
     * Adds a mark to the Board.
     * @param m the NodeMark to be added
     */
	public void addMark(NodeMark m) {
        marks.add(m);
        
        if(panel != null)
        	m.invalidatePanel(panel);
    	
        if(m instanceof SimpleMark) {
    		SimpleMark s = (SimpleMark)m;
    		if(s.getType() < 4)
    			Collections.sort(marks);
    	}
    }
    
	/**
	 * Removes a mark from the Board
	 * @param m the NodeMark to be removed
	 */
    public void removeMark(NodeMark m) {
        if(marks.remove(m))
            m.invalidatePanel(panel);
    }
    
    /**
     * Checks whether there is a label at x,y
     * @param x the x location
     * @param y the y location
     * @return true if there is a NodeLabel at x,y
     */
    public boolean hasLabelAt(int x, int y) {
        for(int k = 0; k < marks.size(); k++) {
            NodeMark m = marks.get(k);
            if(m.getType() == NodeMark.LABEL) {
                NodeLabel lab = (NodeLabel)m;
                Point p = lab.getPoint();
                if(p.x == x && p.y == y)
                    return true;
            }
        }
        return false;
    }
    
    /**
     * Gets the stone at x,y on the Board.
     * @param x the x location
     * @param y the y location
     * @return 1 for black, 0 for empty, -1 for white
     */
    public int getAt(int x, int y) { return board[x][y]; }
    
    /**
     * Sets the stone at x,y on the Board.
     * @param x the x location
     * @param y the y location
     * @param color 1 for black, 0 for empty, -1 for white
     */
    public void setAt(int x, int y, int color) { board[x][y] = color; }
    
    /**
     * Returns the size of the Board.
     * @return the size of the Board
     */
    public int getSize() { return size; }
    
    /**
     * Sets the list of correct next moves.
     * @param g the list of good moves
     */
    public void setGoodMoves(ArrayList<Point> g) { goodMoves = g; }
    
    /**
     * Returns the list of correct next moves.
     * @return the list of correct moves
     */
    public ArrayList<Point> getGoodMoves() { return goodMoves; }
    
    /**
     * Returns the list of incorrect next moves.
     * @param b the list of incorrect moves
     */
    public void setBadMoves(ArrayList<Point> b) { badMoves = b; }
    
    /**
     * Gets the list of incorrect next moves.
     * @return the list of incorrect moves
     */
    public ArrayList<Point> getBadMoves() { return badMoves; }
    
    /**
     * Returns the list of marks on the Board.
     * @return the list of marks
     */
    public ArrayList<NodeMark> getMarks() { return marks; }
    
    /**
     * Sets the marks on the Board.
     * @param m the list of marks
     */
	public void setMarks(ArrayList<NodeMark> m) { marks = new ArrayList<NodeMark>(m); }
    
    /**
     * Returns the ko point - the point where it is illegal to play the next move,
     * if any.
     * @return the ko point
     */
    public Point getKoPoint() { return ko; }
    
    /**
     * Sets the ko point - the point where it is illegal to play the next move.
     * @param p the ko point
     */
    public void setKoPoint(Point p) { ko.x = p.x; ko.y = p.y; }

    /** The file format revision. */
	private static int REVISION = 1;
	
	/**
	 * Reads a Board object from a file.
	 * @param in the input stream
	 * @return the new Board object
	 * @throws IOException if there is a problem reading the file
	 * @throws ClassNotFoundException if there is a problem with the file format
	 */
	public static Board readFromFile(ObjectInputStream in) 
							throws IOException, ClassNotFoundException {
		int revision = in.readInt();
		if(revision > REVISION)
			throw new IOException("File is newer than program");
		
		int size = in.readInt();
		Board b = new Board(size);
		
		for(int i = 0; i < size; i++) {
			for(int j = 0; j < size; j++) {
				b.setAt(i, j, in.readInt());
			}
		}
		
		int numMarks = in.readInt();
		for(int i = 0; i < numMarks; i++)
			b.addMark(NodeMark.readFromFile(in));
		
		return b;
	}
	
	/**
	 * Writes a Board object to a file.
	 * @param out the output stream
	 * @throws IOException if there is a problem writing to the file
	 */
	public void writeToFile(ObjectOutputStream out) throws IOException {
		out.writeInt(REVISION);
		out.writeInt(size);
		
		for(int i = 0; i < size; i++) {
			for(int j = 0; j < size; j++) {
				out.writeInt(board[i][j]);
			}
		}
		
		out.writeInt(marks.size());
		for(NodeMark m : marks)
			NodeMark.writeToFile(out, m);
	}
}
